import { Button, Upload as UploadComp, UploadFile, UploadProps as OriginUploadProps } from 'antd';
import { forwardRef, useMemo, useRef } from 'react';
import { useRefCallback } from '../../../util';
import { useAttachment } from '../AttachmentContext';
import { asyncPool, getFileMd5BySlice } from '../util';

export interface chunkData {
  file: Blob;
  chunkNumber: number;
  chunkSize: number;
  currentChunkSize: number;
  totalChunks: number;
  totalSize: number;
  md5: string;
  fileName: string;
}
export interface ProgressData {
  fileName: string;
  fileSize: number;
  percent: number;
  uploadedSize: number;
  status: UploadFile['status'] | 'fail';
}
const defaultUploadState: {
  success: number;
  fail: number;
  error: number;
  all: number;
  progress: Record<string, ProgressData>;
} = {
  success: 0,
  fail: 0,
  error: 0,
  all: 0,
  progress: {}
};

export interface UploadProps
  extends Pick<
    OriginUploadProps,
    'onRemove' | 'listType' | 'fileList' | 'showUploadList' | 'previewFile' | 'onPreview' | 'disabled'
  > {
  accept?: string;
  hidden?: boolean;
  children?: React.ReactNode;
  containerStyles?: React.CSSProperties;

  onBeforeUpload?: (file: File, fileList: File[]) => Promise<boolean>;
  onUploaded?: (file: File) => void;
  onUploadStateChange: (data: typeof defaultUploadState, uploading: boolean) => void;
}

class AsyncPoolUpload {
  public limit: number;
  public _chunkSize: number;
  public _retry: number;

  constructor(limit = 3, chunkSize = 5242880, retry = 3) {
    this.limit = limit;
    this._chunkSize = chunkSize;
    this._retry = retry;
  }

  async upload(
    file: File,
    options: {
      upload: (v: chunkData) => Promise<void>;
      mergeChunks: (md5: string, totalChunks: number) => Promise<{ fileId?: string }>;
      onChunkUploadSuccess?: (value: chunkData) => void;
    }
  ) {
    const { upload, mergeChunks, onChunkUploadSuccess: _onChunkUploadSuccess } = options;
    return new Promise<void>(async (resolve, reject) => {
      try {
        const identifier = await getFileMd5BySlice(file, this._chunkSize);
        const chunks = this._createFileChunk(file, identifier as string);
        let uploadedChunkNumber = 0;
        const onChunkUploadSuccess = (value: chunkData) => {
          uploadedChunkNumber++;
          if (uploadedChunkNumber === chunks.length) {
            if (chunks.length === 1) {
              _onChunkUploadSuccess?.(value);
              resolve();
            } else {
              mergeChunks(identifier as string, chunks.length)
                .then((res) => {
                  if (!res.fileId) {
                    reject('文件合并失败');
                  } else {
                    _onChunkUploadSuccess?.(value);
                    resolve();
                  }
                })
                .catch((err) => reject(err));
            }
          } else {
            _onChunkUploadSuccess?.(value);
          }
        };
        await this._uploadChunks(chunks, upload, onChunkUploadSuccess);
      } catch (error) {
        reject(error);
      }
    });
  }

  _createFileChunk(file: File, identifier: string): chunkData[] {
    let cur = 0;
    const chunkList: Blob[] = [];
    while (cur < file.size) {
      const sliceFile = file.slice(cur, cur + this._chunkSize);
      chunkList.push(sliceFile);
      cur += this._chunkSize;
    }
    return chunkList.map((chunk, index) => ({
      file: chunk,
      chunkNumber: index + 1,
      chunkSize: this._chunkSize,
      currentChunkSize: chunk.size,
      totalChunks: chunkList.length,
      totalSize: file.size,
      md5: identifier,
      fileName: file.name
    }));
  }

  async _uploadChunks(
    chunkList: chunkData[],
    upload: (v: chunkData) => Promise<void>,
    onChunkUploadSuccess?: (value: chunkData) => void,
    retry = this._retry
  ): Promise<void> {
    if (retry < 0) {
      return Promise.reject(chunkList[0].fileName);
    }
    const uploadSuccessChunk: boolean[] = [true];
    await asyncPool(chunkList, this.limit, async (value) => {
      try {
        await upload(value);
        uploadSuccessChunk[value.chunkNumber] = true;
        onChunkUploadSuccess && onChunkUploadSuccess(value);
      } catch (error) {
        if ((error as any).name === 'api') {
          return Promise.reject(value.fileName);
        }
      }
    });
    const failedUploadChunkList = chunkList.filter((v) => !uploadSuccessChunk[v.chunkNumber]);
    return failedUploadChunkList.length
      ? this._uploadChunks(failedUploadChunkList, upload, onChunkUploadSuccess, retry - 1)
      : Promise.resolve();
  }
}

export const Upload = forwardRef<{}, UploadProps>((props, ref) => {
  const {
    accept = '*',
    children = <Button type="primary">上传文件</Button>,
    hidden = false,
    containerStyles = {},
    onBeforeUpload,
    onUploaded,
    onUploadStateChange,
    ...rest
  } = props;
  const { upload, mergeChunks, uploadThreadCount, chunkSize } = useAttachment();
  const asyncPoolUpload = useMemo(
    () => new AsyncPoolUpload(uploadThreadCount, chunkSize),
    [uploadThreadCount, chunkSize]
  );
  const uploadStateRef = useRef(defaultUploadState);

  const changeUploadState = useRefCallback((nextState: typeof defaultUploadState) => {
    uploadStateRef.current = nextState;
    const { success, fail, all, error } = nextState;
    const completed = success + fail + error === all;
    onUploadStateChange?.(nextState, !completed);
  });

  const uploadFile = useRefCallback(async (file: File) => {
    try {
      await asyncPoolUpload.upload(file, {
        upload,
        mergeChunks,
        onChunkUploadSuccess: (data) => {
          const fileProcess = uploadStateRef.current.progress[data.fileName];
          const nextSize = (fileProcess?.uploadedSize || 0) + data.currentChunkSize;
          const percent = Math.floor((nextSize / data.totalSize) * 100);
          changeUploadState({
            ...uploadStateRef.current,
            progress: {
              ...uploadStateRef.current.progress,
              [data.fileName]: {
                fileSize: data.totalSize,
                fileName: data.fileName,
                uploadedSize: nextSize,
                percent,
                status: percent === 100 ? 'done' : 'uploading'
              }
            }
          });
        }
      });
      changeUploadState({
        ...uploadStateRef.current,
        success: uploadStateRef.current.success + 1
      });
      onUploaded && onUploaded(file);
    } catch (error) {
      changeUploadState({
        ...uploadStateRef.current,
        fail: uploadStateRef.current.fail + 1,
        progress: {
          ...uploadStateRef.current.progress,
          [file.name]: {
            ...uploadStateRef.current.progress[file.name],
            status: 'fail'
          }
        }
      });
    }
  });

  return (
    <div
      style={{
        ...containerStyles,
        display: hidden ? 'none' : containerStyles.display || 'inline-block'
      }}
    >
      <UploadComp
        ref={ref}
        multiple
        accept={accept}
        maxCount={1}
        showUploadList={false}
        beforeUpload={async (file, fileList) => {
          let error = 0;
          if (onBeforeUpload) {
            const checked = await onBeforeUpload(file, fileList);
            error = checked ? 0 : 1;
          }
          const fileProcess = {
            fileSize: file.size,
            fileName: file.name,
            uploadedSize: 0,
            percent: file.size > chunkSize ? 0 : Math.floor(Math.random() * 20) + 30,
            status: (error === 1 ? 'error' : 'uploading') as UploadFile['status']
          };
          const nextState =
            file === fileList[0]
              ? {
                  ...defaultUploadState,
                  all: fileList.length,
                  error: error,
                  progress: {
                    [file.name]: fileProcess
                  }
                }
              : {
                  ...uploadStateRef.current,
                  error: uploadStateRef.current.error + error,
                  progress: {
                    ...uploadStateRef.current.progress,
                    [file.name]: fileProcess
                  }
                };
          changeUploadState(nextState);
          if (error === 1) {
            return false;
          }
          await uploadFile(file);
          return false;
        }}
        children={children}
        {...rest}
      />
    </div>
  );
});
